Open keyboard automatically on Android magic code screen#90771
Conversation
…oid keyboard Replaces the deprecated InteractionManager.runAfterInteractions approach with React Navigation's transitionEnd event listener — the canonical replacement recommended in contributingGuides/INTERACTION_MANAGER.md and the same pattern used by useAutoFocusInput across 100+ screens. Falls back to setTimeout(CONST.SCREEN_TRANSITION_END_TIMEOUT) when transitionEnd does not fire (e.g. when the screen is already focused).
Codecov Report❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.
|
|
@gijoe0295 Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
| }; | ||
|
|
||
| jest.mock('@react-navigation/native', () => { | ||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment |
There was a problem hiding this comment.
❌ CONSISTENCY-5 (docs)
The eslint-disable-next-line @typescript-eslint/no-unsafe-assignment directive lacks a justification comment explaining why the rule is disabled.
Add a comment explaining the reason, e.g.:
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- jest.requireActual returns unknown; safe cast for test mock
const actualNav = jest.requireActual('@react-navigation/native');Reviewed at: 0ac8e59 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
There was a problem hiding this comment.
Applied in 0dd1382 — added a justification comment to the eslint-disable directive explaining that jest.requireActual returns unknown and this is the standard pattern for spreading the actual module in jest.mock factories.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0ac8e59061
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // Android only opens the soft keyboard once the app window has focus, so we | ||
| // chain: wait for the screen transition to finish, then for the window-focus | ||
| // signal (a no-op on iOS and web), then focus the input. isCancelled guards | ||
| // against a late isWindowReadyToFocus resolution stealing focus after the | ||
| // screen has unfocused (e.g. window was blurred when transitionEnd fired). |
There was a problem hiding this comment.
| // Android only opens the soft keyboard once the app window has focus, so we | |
| // chain: wait for the screen transition to finish, then for the window-focus | |
| // signal (a no-op on iOS and web), then focus the input. isCancelled guards | |
| // against a late isWindowReadyToFocus resolution stealing focus after the | |
| // screen has unfocused (e.g. window was blurred when transitionEnd fired). | |
| // Android only opens the soft keyboard once the app window has focus. |
There was a problem hiding this comment.
Done in 0864fee — trimmed to the single line. Kept one short inline comment on the isCancelled check since that guard's reason (avoiding a late focus-steal after blur) isn't obvious from the code alone.
| }); | ||
| }; | ||
|
|
||
| const unsubscribeTransitionEnd = navigation.addListener?.('transitionEnd', (event) => { |
There was a problem hiding this comment.
Let's add a null check here too navigation && navigation.addListener.
There was a problem hiding this comment.
Done in 0864fee — changed to navigation?.addListener?.('transitionEnd', ...) so it short-circuits if navigation is null, matching the codebase's optional-chaining style.
| // Fallback in case `transitionEnd` does not fire (e.g. when the screen is | ||
| // already focused while this effect runs). |
There was a problem hiding this comment.
| // Fallback in case `transitionEnd` does not fire (e.g. when the screen is | |
| // already focused while this effect runs). | |
| // Fallback in case `transitionEnd` does not fire. |
There was a problem hiding this comment.
Don't need this test. This is niche
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppScreen.Recording.2026-05-25.at.22.07.19.movAndroid: mWeb ChromeScreen.Recording.2026-05-25.at.22.38.55.moviOS: HybridAppScreen.Recording.2026-05-25.at.22.42.20.moviOS: mWeb SafariScreen.Recording.2026-05-25.at.22.44.06.movMacOS: Chrome / SafariScreen.Recording.2026-05-25.at.22.45.39.mov |
|
@wildan-m Rodrigo should be back from OOO soon. Can you merge |
…d-keyboard-magic-code
|
@gijoe0295 Merged main in 03ea049 |
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🚧 @rlinoz has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/rlinoz in version: 9.4.1-0 🚀
Bundle Size Analysis (Sentry): |
Help site review: no changes requiredI reviewed the changes in this PR and no updates to the help site ( Why: This PR is a purely technical fix in a single file ( The change:
Since there's nothing for the help site to document differently, I did not open a docs PR. @wildan-m, if you believe a help site update is still warranted, let me know what behavior should be documented and I'll draft one. Otherwise, no |
|
🚀 Deployed to production by https://github.com/Julesssss in version: 9.4.1-6 🚀
|
Explanation of Change
On Android, the soft keyboard did not open automatically when reaching the magic code input screen after adding a new contact method. The form was relying on a fixed 300 ms delay before focusing the input, which has no relationship to whether the app window actually holds focus at that moment. Android's documented behavior is to silently drop the keyboard request whenever the window has not regained focus by the time the focus call is made, so the keyboard never appeared. iOS and web were unaffected because they do not have the same window-focus requirement.
The fix replaces the fixed delay with two combined signals. First, the form listens for the navigation transition-end event so it focuses the input exactly when the screen transition finishes (deterministic instead of a guessed timeout). Then it awaits a small helper that resolves only once the app window has regained focus — a no-op on iOS and web, and on Android a Promise tied to the system's window focus events. A safety fallback timer fires if the transition-end event never arrives (for example when the screen is already focused while the effect runs), and a guard flag ensures the listener and the fallback never both run.
The mobile Safari path is intentionally left as a synchronous focus call, because that browser only opens the keyboard when the focus call originates inside the user gesture handler.
Fixed Issues
$ #80025
PROPOSAL: #80025 (comment)
Tests
Offline tests
The fix only changes when and how the magic code input is focused after a screen transition; it does not alter any network behavior. With the device offline, opening the magic code screen still focuses the input and opens the keyboard exactly as in the online case (verification of the magic code itself still fails offline as before, since that requires a network request).
QA Steps
Same as Tests.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Kapture.2026-05-16.at.20.15.14.mp4
Android: mWeb Chrome
Kapture.2026-05-16.at.20.24.15.mp4
iOS: Native
Kapture.2026-05-16.at.20.32.39.mp4
iOS: mWeb Safari
Kapture.2026-05-16.at.20.34.35.mp4
MacOS: Chrome / Safari
Kapture.2026-05-16.at.20.26.57.mp4